/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.spring.boot.process;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.tuple;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import org.activiti.api.model.shared.model.VariableInstance;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.process.runtime.conf.ProcessRuntimeConfiguration;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.engine.ActivitiException;
import org.activiti.spring.boot.security.util.SecurityUtil;
import org.activiti.spring.boot.test.util.ProcessCleanUpUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class ProcessExtensionsIT {

    private static final String INITIAL_VARS_PROCESS = "Process_initialVarsProcess";
    private static final String INITIAL_VARS_WITH_EXPRESSIONS_PROCESS = "initialVarsExpression";

    @Autowired
    private ProcessRuntime processRuntime;

    @Autowired
    private TaskRuntime taskRuntime;

    @Autowired
    private SecurityUtil securityUtil;

    @Autowired
    private ProcessCleanUpUtil processCleanUpUtil;

    @AfterEach
    public void cleanUp() {
        processCleanUpUtil.cleanUpWithAdmin();
    }

    @BeforeEach
    void setUp() {
        securityUtil.logInAs("user");
    }

    @Test
    public void processInstanceHasInitialVariables() {
        ProcessRuntimeConfiguration configuration = processRuntime.configuration();
        assertThat(configuration).isNotNull();

        // start a process with vars then check default and specified vars exist
        ProcessInstance initialVarsProcess = processRuntime.start(
            ProcessPayloadBuilder.start()
                .withProcessDefinitionKey(INITIAL_VARS_PROCESS)
                .withVariable("extraVar", true)
                .withVariable("age", 10)
                .withBusinessKey("my business key")
                .build()
        );

        assertThat(initialVarsProcess).isNotNull();
        assertThat(initialVarsProcess.getStatus()).isEqualTo(ProcessInstance.ProcessInstanceStatus.RUNNING);

        List<VariableInstance> variableInstances = processRuntime.variables(
            ProcessPayloadBuilder.variables().withProcessInstance(initialVarsProcess).build()
        );

        assertThat(variableInstances).isNotNull();
        assertThat(variableInstances).hasSize(4);

        assertThat(variableInstances)
            .extracting("name")
            .contains("extraVar", "name", "age", "birth")
            .doesNotContain("subscribe");

        // cleanup
        processRuntime.delete(ProcessPayloadBuilder.delete(initialVarsProcess));
    }

    @Test
    public void processInstanceHasValidInitialVariables() throws ParseException {
        ProcessRuntimeConfiguration configuration = processRuntime.configuration();
        assertThat(configuration).isNotNull();

        // start a process with vars then check default and specified vars exist
        ProcessInstance initialVarsProcess = processRuntime.start(
            ProcessPayloadBuilder.start()
                .withProcessDefinitionKey(INITIAL_VARS_PROCESS)
                .withVariable("extraVar", true)
                .withVariable("age", 10)
                .withVariable("name", "bob")
                .withVariable("subscribe", true)
                .withVariable("birth", new SimpleDateFormat("yyyy-MM-dd").parse("2009-11-30"))
                .withBusinessKey("my business key")
                .build()
        );

        assertThat(initialVarsProcess).isNotNull();
        assertThat(initialVarsProcess.getStatus()).isEqualTo(ProcessInstance.ProcessInstanceStatus.RUNNING);

        List<VariableInstance> variableInstances = processRuntime.variables(
            ProcessPayloadBuilder.variables().withProcessInstance(initialVarsProcess).build()
        );

        assertThat(variableInstances).isNotNull();
        assertThat(variableInstances).hasSize(5);

        assertThat(variableInstances).extracting("name").contains("extraVar", "name", "age", "birth", "subscribe");

        // cleanup
        processRuntime.delete(ProcessPayloadBuilder.delete(initialVarsProcess));
    }

    @Test
    public void processInstanceFailsWithoutRequiredVariables() {
        ProcessRuntimeConfiguration configuration = processRuntime.configuration();
        assertThat(configuration).isNotNull();

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> {
                processRuntime.start(
                    ProcessPayloadBuilder.start()
                        .withProcessDefinitionKey(INITIAL_VARS_PROCESS)
                        .withVariable("extraVar", true)
                        .build()
                );
            })
            .withMessage("Can't start process '" + INITIAL_VARS_PROCESS + "' without required variables - age");
    }

    @Test
    public void processInstanceFailsIfVariableTypeIncorrect() {
        ProcessRuntimeConfiguration configuration = processRuntime.configuration();
        assertThat(configuration).isNotNull();

        assertThatExceptionOfType(IllegalStateException.class)
            .isThrownBy(() -> {
                processRuntime.start(
                    ProcessPayloadBuilder.start()
                        .withProcessDefinitionKey(INITIAL_VARS_PROCESS)
                        .withVariable("age", true)
                        .withVariable("name", 7)
                        .withVariable("subscribe", "ok")
                        .withVariable("birth", "2007-10-01")
                        .build()
                );
            })
            .withMessage("Variables fail type validation: subscribe, name, age");
    }

    @Test
    public void should_resolveProcessInstanceInitialVariablesExpressions_when_defined() {
        ProcessRuntimeConfiguration configuration = processRuntime.configuration();
        assertThat(configuration).isNotNull();

        ProcessInstance initialVarsProcess = processRuntime.start(
            ProcessPayloadBuilder.start()
                .withProcessDefinitionKey(INITIAL_VARS_WITH_EXPRESSIONS_PROCESS)
                .withVariable("startVariable", "startVariableValue")
                .build()
        );

        assertThat(initialVarsProcess).isNotNull();
        assertThat(initialVarsProcess.getStatus()).isEqualTo(ProcessInstance.ProcessInstanceStatus.RUNNING);

        List<VariableInstance> variableInstances = processRuntime.variables(
            ProcessPayloadBuilder.variables().withProcessInstance(initialVarsProcess).build()
        );

        assertThat(variableInstances)
            .isNotNull()
            .hasSize(5)
            .extracting(VariableInstance::getName, VariableInstance::getValue)
            .containsOnly(
                tuple("definedVar", "predefinedVarValue"),
                tuple("startVariable", "startVariableValue"),
                tuple(
                    "initVarWithExpressionInString",
                    "start var is startVariableValue and defined var is predefinedVarValue and undefinedVar is "
                ),
                tuple("initVarWithExpression", "startVariableValue"),
                tuple("initVarWithUnresolvableExpression", null)
            );

        // cleanup
        processRuntime.delete(ProcessPayloadBuilder.delete(initialVarsProcess));
    }

    @Test
    public void should_mapProcessVariables_when_startEventMappingExists() {
        //given
        ProcessInstance process = processRuntime.start(
            ProcessPayloadBuilder.start()
                .withProcessDefinitionKey("process-b42a166d-605b-4eec-8b96-82b1253666bf")
                .withVariable("Text0xfems", "name_value")
                .withVariable("Text0rvs0o", "email_value")
                .withBusinessKey("my business key")
                .build()
        );

        //when
        List<VariableInstance> variableInstances = processRuntime.variables(
            ProcessPayloadBuilder.variables().withProcessInstance(process).build()
        );

        //then
        assertThat(variableInstances)
            .isNotNull()
            .hasSize(2)
            .extracting(VariableInstance::getName, VariableInstance::getValue)
            .containsOnly(tuple("name", "name_value"), tuple("email", "email_value"));

        //when
        List<VariableInstance> taskVariables = retrieveVariablesForFirstTaskOfProcess(process);

        //then
        assertThat(taskVariables)
            .isNotNull()
            .hasSize(2)
            .extracting(VariableInstance::getName, VariableInstance::getValue)
            .containsOnly(tuple("nameInTask", "name_value"), tuple("emailInTask", "email_value"));
    }

    private List<VariableInstance> retrieveVariablesForFirstTaskOfProcess(ProcessInstance processInstance) {
        Page<Task> tasks = taskRuntime.tasks(
            Pageable.of(0, 1),
            TaskPayloadBuilder.tasksForProcess(processInstance).build()
        );
        assertThat(tasks.getContent()).hasSize(1);
        return taskRuntime.variables(
            TaskPayloadBuilder.variables().withTaskId(tasks.getContent().get(0).getId()).build()
        );
    }

    @Test
    public void should_mapProcessVariables_when_createAndStartProcessAreCalled() {
        //given
        ProcessInstance createdProcess = processRuntime.create(
            ProcessPayloadBuilder.create()
                .withProcessDefinitionKey("process-b42a166d-605b-4eec-8b96-82b1253666bf")
                .withName("TEST")
                .build()
        );

        ProcessInstance startedProcess = processRuntime.startCreatedProcess(
            createdProcess.getId(),
            ProcessPayloadBuilder.start()
                .withProcessDefinitionKey("process-b42a166d-605b-4eec-8b96-82b1253666bf")
                .withVariable("Text0xfems", "name_value")
                .withVariable("Text0rvs0o", "email_value")
                .withBusinessKey("my business key")
                .build()
        );

        //then
        List<VariableInstance> variableInstances = processRuntime.variables(
            ProcessPayloadBuilder.variables().withProcessInstance(startedProcess).build()
        );

        assertThat(variableInstances)
            .isNotNull()
            .hasSize(2)
            .extracting(VariableInstance::getName, VariableInstance::getValue)
            .containsOnly(tuple("name", "name_value"), tuple("email", "email_value"));

        //when
        List<VariableInstance> taskVariables = retrieveVariablesForFirstTaskOfProcess(createdProcess);

        //then
        assertThat(taskVariables)
            .isNotNull()
            .extracting(VariableInstance::getName, VariableInstance::getValue)
            .containsOnly(tuple("nameInTask", "name_value"), tuple("emailInTask", "email_value"));
    }
}
